/*********************************************************************
 * Copyright (C) 2004 Andrew Khan
 * <p>
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * <p>
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * <p>
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ***************************************************************************/

package jxl.biff.drawing;

import jxl.common.Assert;
import jxl.common.Logger;
import jxl.read.biff.Record;
import jxl.write.biff.File;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

/**
 * This class contains the Excel picture data in Escher format for the
 * entire workbook
 */
public class DrawingGroup implements EscherStream {
    /**
     * The logger
     */
    private static Logger logger = Logger.getLogger(DrawingGroup.class);

    /**
     * The escher data read in from file
     */
    private byte[] drawingData;

    /**
     * The top level escher container
     */
    private EscherContainer escherData;

    /**
     * The Bstore container, which contains all the drawing data
     */
    private BStoreContainer bstoreContainer;

    /**
     * The initialized flag
     */
    private boolean initialized;

    /**
     * The list of user added drawings
     */
    private ArrayList drawings;

    /**
     * The number of blips
     */
    private int numBlips;

    /**
     * The number of charts
     */
    private int numCharts;

    /**
     * The number of shape ids used on the second Dgg cluster
     */
    private int drawingGroupId;

    /**
     * Flag which indicates that at least one of the drawings has been omitted
     * from the worksheet
     */
    private boolean drawingsOmitted;

    /**
     * The origin of this drawing group
     */
    private Origin origin;

    /**
     * A hash map of images keyed on the file path, containing the
     * reference count
     */
    private HashMap imageFiles;

    /**
     * A count of the next available object id
     */
    private int maxObjectId;

    /**
     * The maximum shape id so far encountered
     */
    private int maxShapeId;

    /**
     * Constructor
     *
     * @param o the origin of this drawing group
     */
    public DrawingGroup(Origin o) {
        origin = o;
        initialized = o == Origin.WRITE ? true : false;
        drawings = new ArrayList();
        imageFiles = new HashMap();
        drawingsOmitted = false;
        maxObjectId = 1;
        maxShapeId = 1024;
    }

    /**
     * Copy constructor
     * Uses a shallow copy for most things, since as soon as anything
     * is changed, the drawing group is invalidated and all the data blocks
     * regenerated
     *
     * @param dg the drawing group to copy
     */
    public DrawingGroup(DrawingGroup dg) {
        drawingData = dg.drawingData;
        escherData = dg.escherData;
        bstoreContainer = dg.bstoreContainer;
        initialized = dg.initialized;
        drawingData = dg.drawingData;
        escherData = dg.escherData;
        bstoreContainer = dg.bstoreContainer;
        numBlips = dg.numBlips;
        numCharts = dg.numCharts;
        drawingGroupId = dg.drawingGroupId;
        drawingsOmitted = dg.drawingsOmitted;
        origin = dg.origin;
        imageFiles = (HashMap) dg.imageFiles.clone();
        maxObjectId = dg.maxObjectId;
        maxShapeId = dg.maxShapeId;

        // Create this as empty, because all drawings will get added later
        // as part of the sheet copy process
        drawings = new ArrayList();
    }

    /**

     /**
     * Adds in a drawing group record to this drawing group.  The binary
     * data is extracted from the drawing group and added to a single
     * byte array
     *
     * @param mso the drawing group record to add
     */
    public void add(MsoDrawingGroupRecord mso) {
        addData(mso.getData());
    }

    /**
     * Adds a continue record to this drawing group.  the binary data is
     * extracted and appended to the byte array
     *
     * @param cont the continue record
     */
    public void add(Record cont) {
        addData(cont.getData());
    }

    /**
     * Adds the mso record data to the drawing data
     *
     * @param msodata the raw mso data
     */
    private void addData(byte[] msodata) {
        if (drawingData == null) {
            drawingData = new byte[msodata.length];
            System.arraycopy(msodata, 0, drawingData, 0, msodata.length);
            return;
        }

        // Grow the array
        byte[] newdata = new byte[drawingData.length + msodata.length];
        System.arraycopy(drawingData, 0, newdata, 0, drawingData.length);
        System.arraycopy(msodata, 0, newdata, drawingData.length, msodata.length);
        drawingData = newdata;
    }

    /**
     * Adds a drawing to the drawing group
     *
     * @param d the drawing to add
     */
    final void addDrawing(DrawingGroupObject d) {
        drawings.add(d);
        maxObjectId = Math.max(maxObjectId, d.getObjectId());
        maxShapeId = Math.max(maxShapeId, d.getShapeId());
    }

    /**
     * Adds a  chart to the drawing group
     *
     * @param c the chart
     */
    public void add(Chart c) {
        numCharts++;
    }

    /**
     * Adds a drawing from the public, writable interface
     *
     * @param d the drawing to add
     */
    public void add(DrawingGroupObject d) {
        if (origin == Origin.READ) {
            origin = Origin.READ_WRITE;
            BStoreContainer bsc = getBStoreContainer(); // force initialization
            Dgg dgg = (Dgg) escherData.getChildren()[0];
            drawingGroupId = dgg.getCluster(1).drawingGroupId - numBlips - 1;
            numBlips = bsc != null ? bsc.getNumBlips() : 0;

            if (bsc != null) {
                Assert.verify(numBlips == bsc.getNumBlips());
            }
        }

        if (!(d instanceof Drawing)) {
            // Assign a new object id and add it to the list
            //      drawings.add(d);
            maxObjectId++;
            maxShapeId++;
            d.setDrawingGroup(this);
            d.setObjectId(maxObjectId, numBlips + 1, maxShapeId);
            if (drawings.size() > maxObjectId) {
                logger.warn("drawings length " + drawings.size() +
                        " exceeds the max object id " + maxObjectId);
            }
            //      numBlips++;
            return;
        }

        Drawing drawing = (Drawing) d;

        // See if this is referenced elsewhere
        Drawing refImage =
                (Drawing) imageFiles.get(d.getImageFilePath());

        if (refImage == null) {
            // There are no other references to this drawing, so assign
            // a new object id and put it on the hash map
            maxObjectId++;
            maxShapeId++;
            drawings.add(drawing);
            drawing.setDrawingGroup(this);
            drawing.setObjectId(maxObjectId, numBlips + 1, maxShapeId);
            numBlips++;
            imageFiles.put(drawing.getImageFilePath(), drawing);
        } else {
            // This drawing is used elsewhere in the workbook.  Increment the
            // reference count on the drawing, and set the object id of the drawing
            // passed in
            refImage.setReferenceCount(refImage.getReferenceCount() + 1);
            drawing.setDrawingGroup(this);
            drawing.setObjectId(refImage.getObjectId(),
                    refImage.getBlipId(),
                    refImage.getShapeId());
        }
    }

    /**
     * Interface method to remove a drawing from the group
     *
     * @param d the drawing to remove
     */
    public void remove(DrawingGroupObject d) {
        // Unless there are real images or some such, it is possible that
        // a BStoreContainer will not be present.  In that case simply return
        if (getBStoreContainer() == null) {
            return;
        }

        if (origin == Origin.READ) {
            origin = Origin.READ_WRITE;
            numBlips = getBStoreContainer().getNumBlips();
            Dgg dgg = (Dgg) escherData.getChildren()[0];
            drawingGroupId = dgg.getCluster(1).drawingGroupId - numBlips - 1;
        }

        // Get the blip
        EscherRecord[] children = getBStoreContainer().getChildren();
        BlipStoreEntry bse = (BlipStoreEntry) children[d.getBlipId() - 1];

        bse.dereference();

        if (bse.getReferenceCount() == 0) {
            // Remove the blip
            getBStoreContainer().remove(bse);

            // Adjust blipId on the other blips
            for (Iterator i = drawings.iterator(); i.hasNext(); ) {
                DrawingGroupObject drawing = (DrawingGroupObject) i.next();

                if (drawing.getBlipId() > d.getBlipId()) {
                    drawing.setObjectId(drawing.getObjectId(),
                            drawing.getBlipId() - 1,
                            drawing.getShapeId());
                }
            }

            numBlips--;
        }
    }


    /**
     * Initializes the drawing data from the escher record read in
     */
    private void initialize() {
        EscherRecordData er = new EscherRecordData(this, 0);

        Assert.verify(er.isContainer());

        escherData = new EscherContainer(er);

        Assert.verify(escherData.getLength() == drawingData.length);
        Assert.verify(escherData.getType() == EscherRecordType.DGG_CONTAINER);

        initialized = true;
    }

    /**
     * Gets hold of the BStore container from the Escher data
     *
     * @return the BStore container
     */
    private BStoreContainer getBStoreContainer() {
        if (bstoreContainer == null) {
            if (!initialized) {
                initialize();
            }

            EscherRecord[] children = escherData.getChildren();
            if (children.length > 1 &&
                    children[1].getType() == EscherRecordType.BSTORE_CONTAINER) {
                bstoreContainer = (BStoreContainer) children[1];
            }
        }

        return bstoreContainer;
    }

    /**
     * Gets hold of the binary data
     *
     * @return the data
     */
    public byte[] getData() {
        return drawingData;
    }

    /**
     * Writes the drawing group to the output file
     *
     * @param outputFile the file to write to
     * @exception IOException
     */
    public void write(File outputFile) throws IOException {
        if (origin == Origin.WRITE) {
            DggContainer dggContainer = new DggContainer();

            Dgg dgg = new Dgg(numBlips + numCharts + 1, numBlips);

            dgg.addCluster(1, 0);
            dgg.addCluster(numBlips + 1, 0);

            dggContainer.add(dgg);

            int drawingsAdded = 0;
            BStoreContainer bstoreCont = new BStoreContainer();

            // Create a blip entry for each drawing
            for (Iterator i = drawings.iterator(); i.hasNext(); ) {
                Object o = i.next();
                if (o instanceof Drawing) {
                    Drawing d = (Drawing) o;
                    BlipStoreEntry bse = new BlipStoreEntry(d);

                    bstoreCont.add(bse);
                    drawingsAdded++;
                }
            }
            if (drawingsAdded > 0) {
                bstoreCont.setNumBlips(drawingsAdded);
                dggContainer.add(bstoreCont);
            }

            Opt opt = new Opt();

            dggContainer.add(opt);

            SplitMenuColors splitMenuColors = new SplitMenuColors();
            dggContainer.add(splitMenuColors);

            drawingData = dggContainer.getData();
        } else if (origin == Origin.READ_WRITE) {
            DggContainer dggContainer = new DggContainer();

            Dgg dgg = new Dgg(numBlips + numCharts + 1, numBlips);

            dgg.addCluster(1, 0);
            dgg.addCluster(drawingGroupId + numBlips + 1, 0);

            dggContainer.add(dgg);

            BStoreContainer bstoreCont = new BStoreContainer();
            bstoreCont.setNumBlips(numBlips);

            // Create a blip entry for each drawing that was read in
            BStoreContainer readBStoreContainer = getBStoreContainer();

            if (readBStoreContainer != null) {
                EscherRecord[] children = readBStoreContainer.getChildren();
                for (int i = 0; i < children.length; i++) {
                    BlipStoreEntry bse = (BlipStoreEntry) children[i];
                    bstoreCont.add(bse);
                }
            }

            // Create a blip entry for each drawing that has been added
            for (Iterator i = drawings.iterator(); i.hasNext(); ) {
                DrawingGroupObject dgo = (DrawingGroupObject) i.next();
                if (dgo instanceof Drawing) {
                    Drawing d = (Drawing) dgo;
                    if (d.getOrigin() == Origin.WRITE) {
                        BlipStoreEntry bse = new BlipStoreEntry(d);
                        bstoreCont.add(bse);
                    }
                }
            }

            dggContainer.add(bstoreCont);

            Opt opt = new Opt();

            opt.addProperty(191, false, false, 524296);
            opt.addProperty(385, false, false, 134217737);
            opt.addProperty(448, false, false, 134217792);

            dggContainer.add(opt);

            SplitMenuColors splitMenuColors = new SplitMenuColors();
            dggContainer.add(splitMenuColors);

            drawingData = dggContainer.getData();
        }

        MsoDrawingGroupRecord msodg = new MsoDrawingGroupRecord(drawingData);
        outputFile.write(msodg);
    }

    /**
     * Accessor for the number of blips in the drawing group
     *
     * @return the number of blips
     */
    final int getNumberOfBlips() {
        return numBlips;
    }

    /**
     * Gets the drawing data for the given blip id.  Called by the Drawing
     * object
     *
     * @param blipId the blipId
     * @return the drawing data
     */
    byte[] getImageData(int blipId) {
        numBlips = getBStoreContainer().getNumBlips();

        Assert.verify(blipId <= numBlips);
        Assert.verify(origin == Origin.READ || origin == Origin.READ_WRITE);

        // Get the blip
        EscherRecord[] children = getBStoreContainer().getChildren();
        BlipStoreEntry bse = (BlipStoreEntry) children[blipId - 1];

        return bse.getImageData();
    }

    /**
     * Indicates that at least one of the drawings has been omitted from
     * the worksheet

     * @param mso the mso record
     * @param obj the obj record
     */
    public void setDrawingsOmitted(MsoDrawingRecord mso, ObjRecord obj) {
        drawingsOmitted = true;

        if (obj != null) {
            maxObjectId = Math.max(maxObjectId, obj.getObjectId());
        }
    }

    /**
     * Accessor for the drawingsOmitted flag
     *
     * @return TRUE if a drawing has been omitted, FALSE otherwise
     */
    public boolean hasDrawingsOmitted() {
        return drawingsOmitted;
    }

    /**
     * Updates this with the appropriate data from the drawing group passed in
     * This is called during the copy process:  this is first initialised as
     * an empty object, but during the copy, the source DrawingGroup may
     * change.  After the copy process, this method is then called to update
     * the relevant fields.  Unfortunately, the copy process required the
     * presence of a drawing group
     *
     * @param dg the drawing group containing the updated data
     */
    public void updateData(DrawingGroup dg) {
        drawingsOmitted = dg.drawingsOmitted;
        maxObjectId = dg.maxObjectId;
        maxShapeId = dg.maxShapeId;
    }
}
